/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.kuangcp.app.ftp;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPHTTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;
import org.apache.commons.net.io.CopyStreamEvent;
import org.apache.commons.net.io.CopyStreamListener;
import org.apache.commons.net.util.TrustManagerUtils;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;

/**
 * https://commons.apache.org/proper/commons-net/
 * <p>
 * This is an example program demonstrating how to use the FTPClient class. This program connects to an FTP server and retrieves the specified file. If the -s
 * flag is used, it stores the local file at the FTP server. Just so you can see what's happening, all reply strings are printed. If the -b flag is used, a
 * binary transfer is assumed (default is ASCII). See below for further options.
 */
@Slf4j
public final class FTPClientExample {

    public static final String USAGE = "Expected Parameters: [options] <hostname> <user> <password> [<remote file> [<local file>]]\n"
            + "\nDefault behavior is to download a file and use ASCII transfer mode.\n" + "\t-a - use local active mode (default is local passive)\n"
            + "\t-A - anonymous login (omit user and password parameters)\n" + "\t-b - use binary transfer mode\n"
            + "\t-c cmd - issue arbitrary command (remote is used as a parameter if provided) \n"
            + "\t-d - list directory details using MLSD (remote is used as the pathname if provided)\n" + "\t-e - use EPSV with IPv4 (default false)\n"
            + "\t-E - encoding to use for control channel\n" + "\t-f - issue FEAT command (remote and local files are ignored)\n"
            + "\t-h - list hidden files (applies to -l and -n only)\n" + "\t-i - issue SIZE command for a file\n"
            + "\t-k secs - use keep-alive timer (setControlKeepAliveTimeout)\n" + "\t-l - list files using LIST (remote is used as the pathname if provided)\n"
            + "\t     Files are listed twice: first in raw mode, then as the formatted parsed data.\n"
            + "\t     N.B. if the wrong server-type is used, output may be lost. Use -U or -S as necessary.\n"
            + "\t-L - use lenient future dates (server dates may be up to 1 day into future)\n"
            + "\t-m - list file details using MDTM (remote is used as the pathname if provided)\n"
            + "\t-n - list file names using NLST (remote is used as the pathname if provided)\n"
            + "\t-p true|false|protocol[,true|false] - use FTPSClient with the specified protocol and/or isImplicit setting\n"
            + "\t-s - store file on server (upload)\n" + "\t-S - systemType set server system type (e.g. UNIX VMS WINDOWS)\n"
            + "\t-t - list file details using MLST (remote is used as the pathname if provided)\n" + "\t-U - save unparseable responses\n"
            + "\t-w msec - wait time for keep-alive reply (setControlKeepAliveReplyTimeout)\n"
            + "\t-T  all|valid|none - use one of the built-in TrustManager implementations (none = JVM default)\n"
            + "\t-y format - set default date format string\n" + "\t-Y format - set recent date format string\n"
            + "\t-Z timezone - set the server time zone for parsing LIST responses\n"
            + "\t-z timezone - set the time zone for displaying MDTM, LIST, MLSD, MLST responses\n"
            + "\t-PrH server[:port] - HTTP Proxy host and optional port[80] \n" + "\t-PrU user - HTTP Proxy server user\n"
            + "\t-PrP password - HTTP Proxy server password\n" + "\t-# - add hash display during transfers\n";

    private static CopyStreamListener createListener() {
        return new CopyStreamListener() {
            private long megsTotal;

            @Override
            public void bytesTransferred(final CopyStreamEvent event) {
                bytesTransferred(event.getTotalBytesTransferred(), event.getBytesTransferred(), event.getStreamSize());
            }

            @Override
            public void bytesTransferred(final long totalBytesTransferred, final int bytesTransferred, final long streamSize) {
                final long megs = totalBytesTransferred / 1000000;
                for (long l = megsTotal; l < megs; l++) {
                    System.err.print("#");
                }
                megsTotal = megs;
            }
        };
    }

    boolean storeFile = false, binaryTransfer = false, error = false, listFiles = false, listNames = false, hidden = false;
    boolean localActive = false, useEpsvWithIPv4 = false, feat = false, printHash = false;
    boolean mlst = false, mlsd = false, mdtm = false, saveUnparseable = false;
    boolean size = false;
    boolean lenient = false;
    long keepAliveTimeoutSeconds = -1;
    int controlKeepAliveReplyTimeoutMillis = -1;
    int minParams = 5; // listings require 3 params
    String protocol = null; // SSL protocol
    String doCommand = null;
    String trustmgr = null;
    String proxyHost = null;
    int proxyPort = 80;
    String proxyUser = null;
    String proxyPassword = null;
    String user = null;
    String password = null;
    String encoding = null;
    String serverTimeZoneId = null;
    String displayTimeZoneId = null;
    String serverType = null;
    String defaultDateFormat = null;
    String recentDateFormat = null;

    private void run(final String[] args) throws UnknownHostException {
        int base = parseArgs(args);

        String server = args[base++];
        int port = 0;
        final String[] parts = server.split(":");
        if (parts.length == 2) {
            server = parts[0];
            port = Integer.parseInt(parts[1]);
        }
        if (user == null) {
            user = args[base++];
            password = args[base++];
        }

        String remote = null;
        if (args.length - base > 0) {
            remote = args[base++];
        }

        String local = null;
        if (args.length - base > 0) {
            local = args[base];
        }

        final FTPClient ftp;
        if (protocol == null) {
            if (proxyHost != null) {
                System.out.println("Using HTTP proxy server: " + proxyHost);
                ftp = new FTPHTTPClient(proxyHost, proxyPort, proxyUser, proxyPassword);
            } else {
                ftp = new FTPClient();
            }
        } else {
            final FTPSClient ftps;
            if (protocol.equals("true")) {
                ftps = new FTPSClient(true);
            } else if (protocol.equals("false")) {
                ftps = new FTPSClient(false);
            } else {
                final String[] prot = protocol.split(",");
                if (prot.length == 1) { // Just protocol
                    ftps = new FTPSClient(protocol);
                } else { // protocol,true|false
                    ftps = new FTPSClient(prot[0], Boolean.parseBoolean(prot[1]));
                }
            }
            ftp = ftps;
            if ("all".equals(trustmgr)) {
                ftps.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
            } else if ("valid".equals(trustmgr)) {
                ftps.setTrustManager(TrustManagerUtils.getValidateServerCertificateTrustManager());
            } else if ("none".equals(trustmgr)) {
                ftps.setTrustManager(null);
            }
        }

        if (printHash) {
            ftp.setCopyStreamListener(createListener());
        }
        if (keepAliveTimeoutSeconds >= 0) {
            ftp.setControlKeepAliveTimeout(keepAliveTimeoutSeconds);
        }
        if (controlKeepAliveReplyTimeoutMillis >= 0) {
            ftp.setControlKeepAliveReplyTimeout(controlKeepAliveReplyTimeoutMillis);
        }
        if (encoding != null) {
            ftp.setControlEncoding(encoding);
        }
        ftp.setListHiddenFiles(hidden);

        // suppress login details
        ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));

        final FTPClientConfig config;
        if (serverType != null) {
            config = new FTPClientConfig(serverType);
        } else {
            config = new FTPClientConfig();
        }
        config.setUnparseableEntries(saveUnparseable);
        if (defaultDateFormat != null) {
            config.setDefaultDateFormatStr(defaultDateFormat);
        }
        if (recentDateFormat != null) {
            config.setRecentDateFormatStr(recentDateFormat);
        }
        ftp.configure(config);

        try {
            final int reply;
            if (port > 0) {
                ftp.connect(server, port);
            } else {
                ftp.connect(server);
            }
            System.out.println("Connected to " + server + " on " + (port > 0 ? port : ftp.getDefaultPort()));

            // After connection attempt, you should check the reply code to verify
            // success.
            reply = ftp.getReplyCode();

            if (!FTPReply.isPositiveCompletion(reply)) {
                ftp.disconnect();
                System.err.println("FTP server refused connection.");
                System.exit(1);
            }
        } catch (final IOException e) {
            if (ftp.isConnected()) {
                try {
                    ftp.disconnect();
                } catch (final IOException f) {
                    // do nothing
                }
            }
            System.err.println("Could not connect to server.");
            log.error("", e);
            System.exit(1);
        }

        __main:
        try {
            if (!ftp.login(user, password)) {
                ftp.logout();
                error = true;
                break __main;
            }

            System.out.println("Remote system is " + ftp.getSystemType());

            if (binaryTransfer) {
                ftp.setFileType(FTP.BINARY_FILE_TYPE);
            } else {
                // in theory this should not be necessary as servers should default to ASCII,
                // but they don't all do so - see NET-500
                ftp.setFileType(FTP.ASCII_FILE_TYPE);
            }

            // Use passive mode as default because most of us are
            // behind firewalls these days.
            if (localActive) {
                ftp.enterLocalActiveMode();
            } else {
                ftp.enterLocalPassiveMode();
            }

            ftp.setUseEPSVwithIPv4(useEpsvWithIPv4);

            if (storeFile) {
                try (final InputStream input = new FileInputStream(local)) {
                    ftp.storeFile(remote, input);
                }

                if (keepAliveTimeoutSeconds > 0) {
                    showCslStats(ftp);
                }
            }
            // Allow multiple list types for single invocation
            else if (listFiles || mlsd || mdtm || mlst || listNames || size) {
                if (mlsd) {
                    for (final FTPFile f : ftp.mlistDir(remote)) {
                        System.out.println(f.getRawListing());
                        System.out.println(f.toFormattedString(displayTimeZoneId));
                    }
                }
                if (mdtm) {
                    final FTPFile f = ftp.mdtmFile(remote);
                    if (f != null) {
                        System.out.println(f.getRawListing());
                        System.out.println(f.toFormattedString(displayTimeZoneId));
                    } else {
                        System.out.println("File not found");
                    }
                }
                if (mlst) {
                    final FTPFile f = ftp.mlistFile(remote);
                    if (f != null) {
                        System.out.println(f.toFormattedString(displayTimeZoneId));
                    }
                }
                if (listNames) {
                    for (final String s : ftp.listNames(remote)) {
                        System.out.println(s);
                    }
                }
                if (size) {
                    // TODO ?
//                    System.out.println("Size=" + ftp.getSize(remote));
                }
                // Do this last because it changes the client
                if (listFiles) {
                    if (lenient || serverTimeZoneId != null) {
                        config.setLenientFutureDates(lenient);
                        if (serverTimeZoneId != null) {
                            config.setServerTimeZoneId(serverTimeZoneId);
                        }
                        ftp.configure(config);
                    }

                    for (final FTPFile f : ftp.listFiles(remote)) {
                        System.out.println(f.getRawListing());
                        System.out.println(f.toFormattedString(displayTimeZoneId));
                    }
                }
            } else if (feat) {
                // boolean feature check
                if (remote != null) { // See if the command is present
                    if (ftp.hasFeature(remote)) {
                        System.out.println("Has feature: " + remote);
                    } else if (FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
                        System.out.println("FEAT " + remote + " was not detected");
                    } else {
                        System.out.println("Command failed: " + ftp.getReplyString());
                    }

                    // Strings feature check
                    final String[] features = ftp.featureValues(remote);
                    if (features != null) {
                        for (final String f : features) {
                            System.out.println("FEAT " + remote + "=" + f + ".");
                        }
                    } else if (FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
                        System.out.println("FEAT " + remote + " is not present");
                    } else {
                        System.out.println("Command failed: " + ftp.getReplyString());
                    }
                } else if (ftp.features()) {
//                        Command listener has already printed the output
                } else {
                    System.out.println("Failed: " + ftp.getReplyString());
                }
            } else if (doCommand != null) {
                if (ftp.doCommand(doCommand, remote)) {
//                  Command listener has already printed the output
//                    for(String s : ftp.getReplyStrings()) {
//                        System.out.println(s);
//                    }
                } else {
                    System.out.println("Failed: " + ftp.getReplyString());
                }
            } else {
                try (final OutputStream output = new FileOutputStream(local)) {
                    ftp.retrieveFile(remote, output);
                }

                if (keepAliveTimeoutSeconds > 0) {
                    showCslStats(ftp);
                }
            }

            ftp.noop(); // check that control connection is working OK

            ftp.logout();
        } catch (final FTPConnectionClosedException e) {
            error = true;
            System.err.println("Server closed connection.");
            log.error("", e);
        } catch (final IOException e) {
            error = true;
            log.error("", e);
        } finally {
            if (ftp.isConnected()) {
                try {
                    ftp.disconnect();
                } catch (final IOException f) {
                    // do nothing
                }
            }
        }

        System.exit(error ? 1 : 0);
    }

    private int parseArgs(String[] args) throws UnknownHostException {
        int base;
        label:
        for (base = 0; base < args.length; base++) {
            switch (args[base]) {
                case "-s":
                    storeFile = true;
                    break;
                case "-a":
                    localActive = true;
                    break;
                case "-A":
                    user = "anonymous";
                    password = System.getProperty("user.name") + "@" + InetAddress.getLocalHost().getHostName();
                    break;
                case "-b":
                    binaryTransfer = true;
                    break;
                case "-c":
                    doCommand = args[++base];
                    minParams = 3;
                    break;
                case "-d":
                    mlsd = true;
                    minParams = 3;
                    break;
                case "-e":
                    useEpsvWithIPv4 = true;
                    break;
                case "-E":
                    encoding = args[++base];
                    break;
                case "-f":
                    feat = true;
                    minParams = 3;
                    break;
                case "-h":
                    hidden = true;
                    break;
                case "-i":
                    size = true;
                    minParams = 3;
                    break;
                case "-k":
                    keepAliveTimeoutSeconds = Long.parseLong(args[++base]);
                    break;
                case "-l":
                    listFiles = true;
                    minParams = 3;
                    break;
                case "-m":
                    mdtm = true;
                    minParams = 3;
                    break;
                case "-L":
                    lenient = true;
                    break;
                case "-n":
                    listNames = true;
                    minParams = 3;
                    break;
                case "-p":
                    protocol = args[++base];
                    break;
                case "-S":
                    serverType = args[++base];
                    break;
                case "-t":
                    mlst = true;
                    minParams = 3;
                    break;
                case "-U":
                    saveUnparseable = true;
                    break;
                case "-w":
                    controlKeepAliveReplyTimeoutMillis = Integer.parseInt(args[++base]);
                    break;
                case "-T":
                    trustmgr = args[++base];
                    break;
                case "-y":
                    defaultDateFormat = args[++base];
                    break;
                case "-Y":
                    recentDateFormat = args[++base];
                    break;
                case "-Z":
                    serverTimeZoneId = args[++base];
                    break;
                case "-z":
                    displayTimeZoneId = args[++base];
                    break;
                case "-PrH":
                    proxyHost = args[++base];
                    final String[] parts = proxyHost.split(":");
                    if (parts.length == 2) {
                        proxyHost = parts[0];
                        proxyPort = Integer.parseInt(parts[1]);
                    }
                    break;
                case "-PrU":
                    proxyUser = args[++base];
                    break;
                case "-PrP":
                    proxyPassword = args[++base];
                    break;
                case "-#":
                    printHash = true;
                    break;
                default:
                    break label;
            }
        }

        final int remain = args.length - base;
        if (user != null) {
            minParams -= 2;
        }
        // server, user, pass, remote, local [protocol]
        if (remain < minParams) {
            if (args.length > 0) {
                System.err.println("Actual Parameters: " + Arrays.toString(args));
            }
            System.err.println(USAGE);
            System.exit(1);
        }
        return base;
    }

    public static void main(final String[] args) throws UnknownHostException {
        final FTPClientExample client = new FTPClientExample();
        client.run(args);
    }

    private static void showCslStats(final FTPClient ftp) {
//        @SuppressWarnings("deprecation") // debug code
//        final int[] stats = ftp.getCslDebug();
//        System.out.println("CslDebug=" + Arrays.toString(stats));

    }
}
